﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Text;
using System.Linq;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.Configuration;
using Duet.Model.Core.DataInterfaces;
using Duet.Model.Data;
using System.Reflection;
using Intentor.Utilities;

namespace Duet.Model.Core.Util
{
    /// <summary>
    /// Fábrica de containers de injeção de dependência.
    /// </summary>
    public static class ContainerFactory
    {
        /// <summary>Container de injeção de dependência.</summary>
        private static IUnityContainer _container;

        /// <summary>
        /// Representa o registro de um tipo.
        /// </summary>
        private class TypeRegistration
        {
            /// <summary>Tipo origem no registro de mapeamento.</summary>
            public Type TypeFrom { get; set; }
            /// <summary>Tipo destino no registro de mapeamento.</summary>
            public Type TypeTo { get; set; }
        }

        /// <summary>
        /// Obtém o container do domínio da aplicação.
        /// </summary>
        /// <remarks>O caso de mapeamento considerado para obtenção do container leva em conta
        /// apenas um assembly e 2 namespaces (DataInterfaces e Data).</remarks>
        /// <returns>Referência ao container.</returns>
        public static IUnityContainer GetDomainContainer()
        {
            if (_container == null)
            {
                lock (typeof(IUnityContainer))
                {
                    LoadContainer("Domain");
                }
            }

            return _container;
        }

        #region Support

        /// <summary>
        /// Carrega um container.
        /// </summary>
        /// <param name="containerName">Nome do container no arquivo Unity.config.</param>
        private static void LoadContainer(string containerName)
        {
            _container = new UnityContainer();

            string configurationFilePath = ConfigurationManager.AppSettings["UnityConfigFilePath"];
            if (!Path.IsPathRooted(configurationFilePath)) configurationFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, configurationFilePath);

            var map = new ExeConfigurationFileMap { ExeConfigFilename = configurationFilePath };
            var config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);
            var section = (UnityConfigurationSection)config.GetSection("unity");
            section.Configure(_container, containerName);

            LoadMappingsFromAssemblies(section);
        }

        /// <summary>
        /// Realiza mapeamentos entre DataInterfaces e seus objetos de dados.
        /// </summary>
        /// <param name="section">Seção de configuração do Unity.</param>
        private static void LoadMappingsFromAssemblies(UnityConfigurationSection section)
        {
            //Cria objetos de apoio no registro dos dados.
            var registrations = new List<TypeRegistration>();
            //Para cada assembly, carrega seus tipos.
            foreach (var assemblyData in section.Assemblies)
            {
                //Obtém os nomes dos namespaces.                
                var namespaceDataInterfaces = GetNamespace(section, assemblyData.Name, "DataInterfaces");
                var namespaesData = GetNamespace(section, assemblyData.Name, "Data");

                //Obtém todos os tipos nos namespaces.
                var ass = Assembly.Load(assemblyData.Name);
                var typesDataInterfaces = ReflectionHelper.GetTypesInNamespace(ass, namespaceDataInterfaces);
                var typesData = ReflectionHelper.GetTypesInNamespace(ass, namespaesData);

                /*Para cada tipo de DataInterface, pesquisa um tipo de cada correspondente.
                 *NOTA: considera-se que o formato dos nomes segue a convenção recomendada
                 *para uso com o Yamapper:
                 *  DataInterface: I<Entidade>Repository
                 *  Data: <Entidade>Dao
                 */
                foreach (var rep in typesDataInterfaces)
                {
                    //Formato "I<Entidade>Repository".
                    var entityName = rep.Name.Substring(1, (rep.Name.LastIndexOf("Repository") - 1));
                    //Formato: "<Entidade>Dao".
                    var daoName = entityName + "Dao";

                    //Verfica se a Dao existe
                    var dao = typesData.FirstOrDefault(t => t.Name.Equals(daoName));

                    if (dao == null)
                    {
                        throw new ArgumentException(
                            String.Format("Não foi possível encontrar um mapeamento para a Data Interface \"{0}\". Data Access Object \"{1}\" não encontrado.",
                                rep.Name, daoName));
                    }
                    else
                    {
                        registrations.Add(new TypeRegistration() { TypeFrom = rep, TypeTo = dao });
                    }
                }
            }

            //Realiza o registro de todos os mapeamentos.
            foreach (var reg in registrations) _container.RegisterType(reg.TypeFrom, reg.TypeTo);
        }

        /// <summary>
        /// Obtém um namespace da lista informada com base no nome do assembly <paramref name="beingWith"/> e terminação <paramref name="endsWith"/>.
        /// </summary>
        /// <param name="section">Seção de configuração do Unity.</param>
        /// <param name="beingWith">String de início do namespace, geralmente o nome do assembly.</param>
        /// <param name="endsWith">String de terminação do namespace, geralmente "DataInterface" ou "Data".</param>
        /// <returns>Namespace obtido.</returns>
        private static string GetNamespace(UnityConfigurationSection section, string beingWith, string endsWith)
        {
            var nms = String.Empty;

            foreach (var nm in section.Namespaces)
            {
                if (nm.Name.IndexOf(beingWith) == 0 && nm.Name.EndsWith(endsWith))
                {
                    nms = nm.Name;
                    break;
                }
            }

            return nms;
        }

        #endregion
    }
}
